// Copyright (c) 2007 Readify Pty. Ltd.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Microsoft.TeamFoundation.Build.Proxy;
using Microsoft.TeamFoundation.Client;
using DependencyReplicator.Properties;
using Microsoft.TeamFoundation.Server;
using System.Diagnostics;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Xml.Serialization;
using System.IO;
using System.Security.AccessControl;

namespace DependencyReplicator
{
    public class BuildStoreWatcher
    {
        private static TraceSwitch m_TraceSwitch = new TraceSwitch("General", string.Empty);

        private T GetService<T>()
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Getting service '{0}' for server '{1}'.",
                    typeof(T).FullName,
                    Settings.Default.TeamFoundationServerUrl
                    );
            }
#line default
            #endregion

            TeamFoundationServer server = TeamFoundationServerFactory.GetServer(Settings.Default.TeamFoundationServerUrl);
            T service = (T)server.GetService(typeof(T));

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Successfully got service '{0}' from server '{1}'.",
                    typeof(T).FullName,
                    Settings.Default.TeamFoundationServerUrl
                    );
            }
#line default
            #endregion

            return service;
        }

        private Thread m_WorkerThread = null;

        private void Worker()
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Entering worker method."
                    );
            }
#line default
            #endregion

            while (true)
            {
                try
                {
                    #region Tracing
#line hidden
                    if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                    {
                        Trace.TraceInformation(
                            "Starting dependency replication processing."
                            );
                    }
#line default
                    #endregion

                    BuildStoreEvent[] buildStoreEvents = this.GetListOfBuildStoreEvents();

                    if (buildStoreEvents.Length > 0)
                    {
                        RuleCollection rules = this.GetListOfApplicableRules(buildStoreEvents);
                        this.ExecuteDependencyReplicationRules(rules);
                    }
                }
                catch (ThreadAbortException ex)
                {
                    #region Tracing
#line hidden
                    if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                    {
                        Trace.TraceInformation(
                            "Thread abort exception received. Assuming normal shutdown:\n{0}",
                            ex
                            );
                    }
#line default
                    #endregion
                    return;
                }
                catch (Exception ex)
                {
                    #region Tracing
#line hidden
                    if (BuildStoreWatcher.m_TraceSwitch.TraceError)
                    {
                        Trace.TraceError(
                            "An unexpected error occured whilst monitoring the build store:\n{0}",
                            ex
                            );
                    }
#line default
                    #endregion
                }

                Thread.Sleep(Settings.Default.PollPeriod);
            }

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Exiting worker method."
                    );
            }
#line default
            #endregion

        }

        private Workspace EnsureWorkspace(VersionControlServer versionControl)
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Ensuring that the workspace '{0}' at '{1}' is clean.",
                    Settings.Default.WorkspaceName,
                    Settings.Default.WorkspacePath
                    );
            }
#line default
            #endregion

            this.DeleteOldWorkspace(versionControl);
            Workspace workspace = this.CreateNewWorkspace(versionControl);

            return workspace;
        }

        private Workspace CreateNewWorkspace(VersionControlServer versionControl)
        {
            Workspace workspace = versionControl.CreateWorkspace(Settings.Default.WorkspaceName);
            workspace.Map("$/", Settings.Default.WorkspacePath);
            return workspace;
        }

        private void DeleteOldWorkspace(VersionControlServer versionControl)
        {
            Workspace workspace = versionControl.TryGetWorkspace(Settings.Default.WorkspacePath);
            if (workspace == null)
            {
                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceWarning)
                {
                    Trace.TraceWarning(
                        "The expected workspace '{0}' at '{1}' does not exist.",
                        Settings.Default.WorkspaceName,
                        Settings.Default.WorkspacePath
                        );
                }
#line default
                #endregion

                return;
            }

            if (workspace.Name != Settings.Default.WorkspaceName)
            {
                string errorMessage = string.Format(
                    "The workspace at '{0}' has an unexpected name. Expected '{1}', was '{2}'.",
                    Settings.Default.WorkspacePath,
                    Settings.Default.WorkspaceName,
                    workspace.Name
                    );
                throw new Exception(errorMessage);
            }

            bool workspaceDeleted = workspace.Delete();

            if (Directory.Exists(Settings.Default.WorkspacePath))
            {
                this.RecursiveMarkReadWrite(Settings.Default.WorkspacePath);
                Directory.Delete(Settings.Default.WorkspacePath, true);
            }
        }

        private void RecursiveMarkReadWrite(string path)
        {
            #region Tracing
#line hidden
			if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
			{
				Trace.TraceInformation(
					"Recursively marking files in path '{0}' as read-write.",
                    path
					);
			}
#line default
			#endregion

            DirectoryInfo currentDirectory = new DirectoryInfo(path);
            FileInfo[] files = currentDirectory.GetFiles("*.*", SearchOption.AllDirectories);

            #region Tracing
#line hidden
			if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
			{
				Trace.TraceInformation(
					"Found '{0}' files in the directory '{1}'.",
                    files.Length,
                    path
					);
			}
#line default
			#endregion
	
            foreach (FileInfo file in files)
            {
                #region Tracing
#line hidden
			    if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
			    {
				    Trace.TraceInformation(
					    "Marking the file '{0}' as read-write.",
                        file.FullName
					    );
			    }
#line default
			#endregion
                file.IsReadOnly = false;
            }
        }

        private void ExecuteDependencyReplicationRules(RuleCollection rules)
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Executing '{0}' dependency replication rules.",
                    rules.Count
                    );
            }
#line default
            #endregion

            VersionControlServer versionControl = this.GetService<VersionControlServer>();
            Workspace workspace = this.EnsureWorkspace(versionControl);

            foreach (Rule rule in rules)
            {
                this.ExecuteDependencyReplicationRule(rule, workspace);
            }
        }

        private void ExecuteDependencyReplicationRule(Rule rule, Workspace workspace)
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Processing dependency replication rule for '{0}' from build '{1}' in team project '{2}'.",
                    rule.Destination,
                    rule.Event.Data.BuildNumber,
                    rule.Event.Data.TeamProject
                    );
            }
#line default
            #endregion

            try
            {
                string[] getItems = new string[] { rule.Destination };
                GetStatus status = workspace.Get(getItems, VersionSpec.Latest, RecursionType.None, GetOptions.None);

                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Get of '{0}' completed.\nOperations: {1}\nWarnings: {2}\nConflicts: {3}\nFailures: {4}",
                        rule.Destination,
                        status.NumOperations,
                        status.NumWarnings,
                        status.NumConflicts,
                        status.NumFailures
                        );
                }
#line default
                #endregion

                workspace.PendEdit(rule.Destination);

                string fullyQualifiedSource = Path.Combine(rule.Event.Data.DropLocation, rule.Source);
                string fullyQualifiedDestination = workspace.GetLocalItemForServerItem(rule.Destination);
                File.Copy(fullyQualifiedSource, fullyQualifiedDestination, true);

                PendingChange[] changes = workspace.GetPendingChanges();

                string comment = null;
                switch (rule.EventType)
                {
                    case BuildStoreEventType.New:
                        comment = string.Format(
                            "Checking in dependencies after build '{0}' from team project '{1}' completed successfully.",
                            rule.Event.Data.BuildNumber,
                            rule.Event.Data.TeamProject
                            );
                        break;
                    case BuildStoreEventType.QualityChanged:
                        comment = string.Format(
                            "Checking in dependencies after build '{0}' from team project '{1}' after quality was changed to '{2}'.",
                            rule.Event.Data.BuildNumber,
                            rule.Event.Data.TeamProject,
                            rule.Event.Data.BuildQuality
                            );
                        break;
                    default:
                        throw new Exception("Dependency replication event was not recognised.");
                        break;
                }

                int changesetNumber = workspace.CheckIn(changes, comment);

                if (changesetNumber == 0)
                {
                    #region Tracing
#line hidden
                    if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                    {
                        Trace.TraceInformation(
                            "Changeset number '{0}' was returned. This means '{1}' was already up-to-date or identical.",
                            changesetNumber,
                            rule.Destination,
                            rule.Event.Data.BuildNumber,
                            rule.Event.Data.TeamProject
                            );
                    }
#line default
                    #endregion
                }
                else
                {
                    #region Tracing
#line hidden
                    if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                    {
                        Trace.TraceInformation(
                            "Changeset number '{0}' applied to version control containing '{1}' from build '{2}' in team project '{3}'.",
                            changesetNumber,
                            rule.Destination,
                            rule.Event.Data.BuildNumber,
                            rule.Event.Data.TeamProject
                            );
                    }
#line default
                    #endregion
                }
            }
            catch (Exception ex)
            {
                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceError)
                {
                    Trace.TraceError(
                        "An error occured processing the dependency '{1}' from build '{2}' in project '{3}'. Processing subsequent rules will continue:\n{0}",
                        ex,
                        rule.Destination,
                        rule.Event.Data.BuildNumber,
                        rule.Event.Data.TeamProject
                        );
                }
#line default
                #endregion
            }
        }

        private RuleCollection FetchAllRules()
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation("Fetching rule files from version control.");
            }
#line default
            #endregion

            VersionControlServer versionControl = this.GetService<VersionControlServer>();
            TeamProject[] projects = versionControl.GetAllTeamProjects(true);

            RuleCollection allRules = new RuleCollection();

            foreach (TeamProject project in projects)
            {
                RuleCollection projectRules = this.FetchRulesForTeamProject(project, versionControl);
                allRules.AddRange(projectRules);
            }

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Fetched '{0}' rules from '{0}' team projects.",
                    allRules.Count,
                    projects.Length
                    );
            }
#line default
            #endregion

            return allRules;
        }

        private RuleCollection FetchRulesForTeamProject(TeamProject project, VersionControlServer versionControl)
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Fetching rules file for team project '{0}'.",
                    project
                    );
            }
#line default
            #endregion

            RuleCollection rules = new RuleCollection();

            try
            {
                string tempFile = Path.GetTempFileName();
                string serverPath = string.Format("{0}/TeamBuildTypes/DependencyReplicator.xml", project.ServerItem);
                versionControl.DownloadFile(serverPath, tempFile);

                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
                {
                    Trace.TraceInformation(
                        "Downloaded rule file from '{0}' to '{1}'.",
                        serverPath,
                        tempFile
                        );
                }
#line default
                #endregion

                using (FileStream stream = new FileStream(tempFile, FileMode.Open))
                {
                    XmlSerializer serializer = new XmlSerializer(typeof(RuleCollection));
                    rules = (RuleCollection)serializer.Deserialize(stream);
                }

                File.Delete(tempFile);
            }
            catch (Exception ex)
            {
                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceWarning)
                {
                    Trace.TraceWarning(
                        "Could not download rule file for the team project '{0}':\n{1}",
                        project.Name,
                        ex
                        );
                }
#line default
                #endregion
            }

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "The '{0}' team project has '{1}' dependency replication rules defined.",
                    project.Name,
                    rules.Count
                    );
            }
#line default
            #endregion

            return rules;
        }

        private RuleCollection GetListOfApplicableRules(BuildStoreEvent[] buildStoreEvents)
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Getting list of applicable rules for the '{0}' build store events.",
                    buildStoreEvents.Length
                    );
            }
#line default
            #endregion

            RuleCollection applicableRules = new RuleCollection();

            RuleCollection allRules = this.FetchAllRules();
            foreach (Rule rule in allRules)
            {
                foreach (BuildStoreEvent buildStoreEvent in buildStoreEvents)
                {
                    if ((rule.EventType == buildStoreEvent.Type) &&
                        (rule.BuildType == buildStoreEvent.Data.BuildType) &&
                        (rule.EventType == BuildStoreEventType.New))
                    {
                        rule.Event = buildStoreEvent;
                        applicableRules.Add(rule);
                        break;
                    }

                    if ((rule.EventType == buildStoreEvent.Type) &&
                        (rule.BuildType == buildStoreEvent.Data.BuildType) &&
                        (rule.EventType == BuildStoreEventType.QualityChanged) &&
                        (rule.BuildQuality == buildStoreEvent.Data.BuildQuality))
                    {
                        rule.Event = buildStoreEvent;
                        applicableRules.Add(rule);
                        break;
                    }
                }
            }

            return applicableRules;
        }

        private BuildStoreEvent[] GetListOfBuildStoreEvents()
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Getting list of build store events."
                    );
            }
#line default
            #endregion

            string[] teamProjects = this.GetListOfTeamProjects();

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Fetched '{0}' team projects.",
                    teamProjects.Length
                    );
            }
#line default
            #endregion

            BuildData[] builds = this.GetBuildsForTeamProjects(teamProjects);

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Fetched '{0}' build data objects for '{1}' team projects.",
                    builds.Length,
                    teamProjects.Length
                    );
            }
#line default
            #endregion

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Comparing current build store data with local cache."
                    );
            }
#line default
            #endregion

            List<BuildStoreEvent> buildStoreEventList = new List<BuildStoreEvent>();
            foreach (BuildData build in builds)
            {
                BuildStoreEvent buildStoreEvent = this.GetBuildStoreEventIfAny(build);
                if (buildStoreEvent != null)
                {
                    buildStoreEventList.Add(buildStoreEvent);
                }
            }

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "There were '{0}' build store events detected.",
                    buildStoreEventList.Count
                    );
            }
#line default
            #endregion

            BuildStoreEvent[] buildStoreEvents = buildStoreEventList.ToArray();
            return buildStoreEvents;
        }

        private Dictionary<string, BuildData> m_CacheLookup = new Dictionary<string, BuildData>();

        private BuildStoreEvent GetBuildStoreEventIfAny(BuildData build)
        {
            if (build.BuildStatus != Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildSucceeded)
            {
                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
                {
                    Trace.TraceInformation(
                        "Ignoring build '{0}' from '{1}' because it was not completed successfully.",
                        build.BuildNumber,
                        build.TeamProject
                        );
                }
#line default
                #endregion

                return null;
            }

            if (!this.m_CacheLookup.ContainsKey(build.BuildUri))
            {
                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "The build '{0}' from '{1}' was determined to be new. A build store event will be created.",
                        build.BuildNumber,
                        build.TeamProject
                        );
                }
#line default
                #endregion

                this.m_CacheLookup.Add(build.BuildUri, build);

                BuildStoreEvent buildStoreEvent = new BuildStoreEvent();
                buildStoreEvent.Type = BuildStoreEventType.New;
                buildStoreEvent.Data = build;
                return buildStoreEvent;
            }

            BuildData originalBuild = this.m_CacheLookup[build.BuildUri];
            this.m_CacheLookup[build.BuildUri] = build;

            if (originalBuild.BuildQuality != build.BuildQuality)
            {
                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "The build '{0}' from '{1}' has had its quality changed from '{2}' to '{3}'. A build store event will be created.",
                        build.BuildNumber,
                        build.TeamProject,
                        originalBuild.BuildQuality,
                        build.BuildQuality
                        );
                }
#line default
                #endregion

                BuildStoreEvent buildStoreEvent = new BuildStoreEvent();
                buildStoreEvent.Type = BuildStoreEventType.QualityChanged;
                buildStoreEvent.Data = build;
                return buildStoreEvent;
            }

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "The build '{0}' from '{1}' has not had any changes.",
                    build.BuildNumber,
                    build.TeamProject
                    );
            }
#line default
            #endregion

            return null;
        }

        private BuildData[] GetBuildsForTeamProjects(string[] teamProjects)
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Getting a list of builds for '{0}' team projects.",
                    teamProjects.Length
                    );
            }
#line default
            #endregion

            List<BuildData> buildList = new List<BuildData>();

            BuildStore buildStoreService = this.GetService<BuildStore>();
            foreach (string teamProject in teamProjects)
            {
                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
                {
                    Trace.TraceInformation(
                        "Getting list of builds for team project '{0}'.",
                        teamProject
                        );
                }
#line default
                #endregion

                BuildData[] buildDataArray = buildStoreService.GetListOfBuilds(teamProject, null);

                // HACK: For some reason the team project property is not populated.
                Array.ForEach<BuildData>(buildDataArray, delegate(BuildData bd) { bd.TeamProject = teamProject; });

                buildList.AddRange(buildDataArray);

                #region Tracing
#line hidden
                if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "The team project has '{0}' builds.",
                        buildDataArray.Length
                        );
                }
#line default
                #endregion
            }

            BuildData[] builds = buildList.ToArray();
            return builds;
        }

        private string[] GetListOfTeamProjects()
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Fetching a list of team projects."
                    );
            }
#line default
            #endregion

            ICommonStructureService structureService = this.GetService<ICommonStructureService>();
            ProjectInfo[] projectInfoArray = structureService.ListProjects();
            string[] projects = Array.ConvertAll<ProjectInfo, string>(
                projectInfoArray,
                delegate(ProjectInfo input) { return input.Name; }
                );
            return projects;
        }

        public void Start()
        {
            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceVerbose)
            {
                Trace.TraceInformation(
                    "Starting up worker thread."
                    );
            }
#line default
            #endregion

            if (this.m_WorkerThread != null)
            {
                throw new Exception("The worker thread has already been started.");
            }

            this.GetListOfBuildStoreEvents();

            this.m_WorkerThread = new Thread(this.Worker);
            this.m_WorkerThread.IsBackground = true;
            this.m_WorkerThread.Start();

            #region Tracing
#line hidden
            if (BuildStoreWatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Successfully started worker thread."
                    );
            }
#line default
            #endregion
        }

        public void Stop()
        {
            if (this.m_WorkerThread == null)
            {
                throw new Exception("The worker thread was not started.");
            }

            this.m_WorkerThread.Abort();
            this.m_WorkerThread = null;
        }
    }
}
